popiart-server
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

204 lines
8.1 KiB

import Link from "next/link";
import { BillingPurchaseActions } from "@/components/billing-purchase-actions";
import {
getBillingCatalog,
getBillingPointsPackages,
getBillingSubscriptionPlans,
getViewerSession,
} from "@/lib/popiart-api";
import { getDictionary, type Locale } from "@/lib/site-content";
export default async function PricingPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;
const typedLocale = locale as Locale;
const dictionary = getDictionary(typedLocale);
const session = await getViewerSession();
const isZh = typedLocale === "zh";
let plans = dictionary.pricing.plans;
let subscriptionPlans: Awaited<ReturnType<typeof getBillingSubscriptionPlans>> = null;
let pointsPackages: Awaited<ReturnType<typeof getBillingPointsPackages>> = null;
let billingLiveError = "";
const bindHint = isZh
? "如需查看真实套餐与发起支付,请先在控制台绑定网关用户态。"
: "Bind a gateway user in the console first to see live plans and start checkout.";
const purchaseLabels = {
alipay: isZh ? "支付宝支付" : "Pay with Alipay",
wxpay: isZh ? "微信支付" : "Pay with WeChat",
creating: isZh ? "创建中..." : "Creating...",
tradeNo: isZh ? "交易号" : "Trade no",
openLink: isZh ? "打开支付链接" : "Open payment link",
codeUrl: isZh ? "扫码地址" : "Code URL",
invalid: isZh ? "创建支付失败。" : "Failed to create payment.",
pending: isZh ? "支付处理中" : "Payment pending",
success: isZh ? "支付成功" : "Payment successful",
failed: isZh ? "支付失败" : "Payment failed",
paymentStatus: isZh ? "支付状态" : "Payment status",
qrTitle: isZh ? "微信扫码支付" : "Scan with WeChat",
openConsole: isZh ? "前往控制台" : "Open console",
openBilling: isZh ? "查看账单中心" : "Open billing",
};
try {
const catalog = await getBillingCatalog();
if (catalog?.plans?.length) {
plans = catalog.plans.map((plan) => ({
badge: plan.badge,
name: plan.name,
price: plan.price,
cadence: plan.cadence,
summary: plan.summary,
features: plan.features,
cta: plan.cta,
highlight: plan.highlight,
}));
}
} catch {
// Keep the seeded dictionary fallback when the product billing catalog is unavailable.
}
if (session?.gateway_bound) {
try {
const [subscriptionResult, pointsResult] = await Promise.all([
getBillingSubscriptionPlans(),
getBillingPointsPackages(),
]);
subscriptionPlans = subscriptionResult;
pointsPackages = pointsResult;
} catch (error) {
billingLiveError = error instanceof Error ? error.message : String(error);
}
}
return (
<div className="page-stack">
<section className="section-panel hero-tight">
<div className="section-heading">
<div className="eyebrow">{dictionary.pricing.tag}</div>
<h1>{dictionary.pricing.title}</h1>
<p>{dictionary.pricing.subtitle}</p>
</div>
{session ? (
<div className="status-banner">
{session.gateway_bound ? (
<span>{isZh ? "已绑定网关用户,可直接读取真实套餐并发起支付。" : "Gateway user bound. Live plans and checkout are available."}</span>
) : (
<span>{bindHint}</span>
)}
</div>
) : null}
{billingLiveError ? <div className="status-banner status-banner-error">{billingLiveError}</div> : null}
</section>
<section className="pricing-grid">
{(subscriptionPlans?.items.length ? subscriptionPlans.items.map((item) => ({
key: String(item.plan.id),
badge: item.plan.recommended ? (isZh ? "推荐方案" : "Recommended") : (isZh ? "订阅方案" : "Subscription"),
name: item.plan.title,
price: `${item.plan.currency} ${item.plan.price_amount}`,
cadence: `${item.plan.duration_value} ${item.plan.duration_unit}`,
summary: item.plan.description || item.plan.subtitle,
features: [
`${item.plan.points_amount} ${isZh ? "订阅赠送积分" : "subscription points"}`,
`${item.plan.total_amount || 0} ${isZh ? "总额度" : "total quota"}`,
`${isZh ? "会员等级" : "member level"}: ${item.plan.member_level || "-"}`,
`${isZh ? "重置周期" : "reset period"}: ${item.plan.quota_reset_period || "-"}`,
],
cta: isZh ? "购买订阅" : "Buy subscription",
highlight: item.plan.recommended,
itemId: item.plan.id,
kind: "subscription" as const,
})) : plans.map((plan) => ({
key: plan.name,
badge: plan.badge,
name: plan.name,
price: plan.price,
cadence: plan.cadence,
summary: plan.summary,
features: plan.features,
cta: plan.cta,
highlight: plan.highlight,
itemId: 0,
kind: "subscription" as const,
}))).map((plan) => (
<article
className={`pricing-card ${plan.highlight ? "pricing-card-highlight" : ""}`}
key={plan.key}
>
<div className="pricing-top">
<div>
<span className="card-kicker">{plan.badge}</span>
<h2>{plan.name}</h2>
</div>
<div className="price-line">
<strong>{plan.price}</strong>
<span>{plan.cadence}</span>
</div>
</div>
<p className="pricing-summary">{plan.summary}</p>
<ul className="bullet-list">
{plan.features.map((feature) => (
<li key={feature}>{feature}</li>
))}
</ul>
{session?.gateway_bound && plan.itemId > 0 ? (
<BillingPurchaseActions itemId={plan.itemId} kind={plan.kind} labels={purchaseLabels} />
) : (
<Link className="button button-dark" href={`/${locale}/login`}>
{plan.cta}
</Link>
)}
</article>
))}
</section>
{pointsPackages?.items.length ? (
<section className="pricing-grid">
{pointsPackages.items.map((pack) => (
<article className="pricing-card" key={pack.id}>
<div className="pricing-top">
<div>
<span className="card-kicker">{isZh ? "积分包" : "Points pack"}</span>
<h2>{pack.name}</h2>
</div>
<div className="price-line">
<strong>{pack.currency} {pack.price_amount}</strong>
<span>{isZh ? "/包" : "/pack"}</span>
</div>
</div>
<p className="pricing-summary">
{isZh
? `到账 ${pack.points_amount + pack.bonus_points} 积分(含赠送 ${pack.bonus_points}`
: `${pack.points_amount + pack.bonus_points} total points (${pack.bonus_points} bonus)`}
</p>
<ul className="bullet-list">
<li>{isZh ? `基础积分 ${pack.points_amount}` : `Base points ${pack.points_amount}`}</li>
<li>{isZh ? `赠送积分 ${pack.bonus_points}` : `Bonus points ${pack.bonus_points}`}</li>
<li>{isZh ? "购买成功后直接进入积分钱包" : "Delivered into your point wallets after payment"}</li>
</ul>
<BillingPurchaseActions itemId={pack.id} kind="points" labels={purchaseLabels} />
</article>
))}
</section>
) : null}
<section className="section-panel">
<div className="section-heading">
<div className="eyebrow">{dictionary.pricing.faqTag}</div>
<h2>{dictionary.pricing.faqTitle}</h2>
</div>
<div className="faq-grid">
{dictionary.pricing.faqs.map((faq) => (
<article className="faq-card" key={faq.question}>
<h3>{faq.question}</h3>
<p>{faq.answer}</p>
</article>
))}
</div>
</section>
</div>
);
}